{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Part 1, Topic 4: Authenticated AES Bootloader"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "NOTE: This lab references some (commercial) training material on [ChipWhisperer.io](https://www.ChipWhisperer.io). You can freely execute and use the lab per the open-source license (including using it in your own courses if you distribute similarly), but you must maintain notice about this source location. Consider joining our training course to enjoy the full experience.\n",
    "\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**SUMMARY:** *In the previous lab, we saw how glitching might be used to get a bootloader to dump it's entire memory by glitching it's response to a command.*\n",
    "\n",
    "*In this lab, we'll again be targeting a bootloader. This time, however, we have a different objective. Instead of trying to read firmware, we'll be trying to bypass the target's authentication in order to upload our own firmware.*\n",
    "\n",
    "**LEARNING OUTCOMES:**\n",
    "\n",
    "* Using glitching to bypass authentication of a bootloader command\n",
    "* Performing a CPA attack using only traces that bypassed authentication\n",
    "\n",
    "**Prerequisites**\n",
    "This lab requires you to do a CPA attack on the bootloader. If you haven't yet, it's recommended that you run through SCA101 before attempting this lab."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Target Bootloader\n",
    "\n",
    "Throughout SCA101 (and even SCA201), we've focused exclusively on unauthenticated encryption. In real systems, this is often a very bad idea. If you're interested, the following blog post outlines some basic attacks on unauthenticated encryption: https://cybergibbons.com/reverse-engineering-2/why-is-unauthenticated-encryption-insecure/.\n",
    "\n",
    "For our bootloader example, it would be catastrophic if we were able to modify the resulting firmware. We could then, for example, inject malicious code to dump out the firmware or encryption key. To try to prevent this, our target appends a message authentication code to each encrypted frame in an [Encrypt-then-MAC scheme](https://en.wikipedia.org/wiki/Authenticated_encryption#Encrypt-then-MAC). To keep things simple, the \"hash function\" is just an 8-bit CRC. When calculating the MAC (Message Authentication Code), the ciphertext is appended to a 64-bit key. Note  that this scheme is **not** secure; however, it is simple and it lets us reuse code from other parts of ChipWhisperer. In total, the whole frame this looks like:\n",
    "\n",
    "* 16 bytes of encrypted (AES128-CBC) firmware\n",
    "* 1 byte MAC\n",
    "\n",
    "The frame is then wrapped in our standard SimpleSerial packet we'll be using the 'p' command (or 0x01 on SSV2) to send \n",
    "a frame to the target.\n",
    "\n",
    "To start, setup the hardware as usual:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "SCOPETYPE = 'OPENADC'\n",
    "PLATFORM = 'CWLITEARM'\n",
    "SS_VER = 'SS_VER_2_1'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%run \"../../Setup_Scripts/Setup_Generic.ipynb\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%bash -s \"$PLATFORM\" \"$SS_VER\"\n",
    "cd ../../../firmware/mcu/simpleserial-aes-bootloader\n",
    "make PLATFORM=$1 CRYPTO_TARGET=TINYAES128C SS_VER=$2 -j"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fw_path = \"../../../firmware/mcu/simpleserial-aes-bootloader/simpleserial-bootloader-{}.hex\".format(PLATFORM)\n",
    "cw.program_target(scope, prog, fw_path)\n",
    "target.reset_comms()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can pull in the CRC calculation from the SimpleSerial2 class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "calc_crc = cw.targets.SimpleSerial2._calc_crc"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(calc_crc([0x00, 0x12]))\n",
    "\n",
    "def bootloader_calc_crc(buf):\n",
    "    mac_key = [0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6]\n",
    "    crc_buf = bytearray(mac_key)\n",
    "    crc_buf.extend(buf)\n",
    "    return calc_crc(crc_buf)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ktp = cw.ktp.Basic()\n",
    "key, text = ktp.next()\n",
    "\n",
    "print(bootloader_calc_crc(text))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We'll probably crash the target a few times while we're trying some glitching. Create a function to reset the target:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def reboot_flush():\n",
    "    reset_target(scope)\n",
    "    target.reset_comms()\n",
    "    target.simpleserial_write(0x00, bytearray(range(16)))\n",
    "    target.simpleserial_wait_ack()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We want to catch both the MAC calculation and the beginning of AES, so we'll set `adc.samples` to its max:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "scope.adc.samples = 24400\n",
    "scope.adc.offset = 5000"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Communication\n",
    "\n",
    "To start off, we need to inititalize the bootloader with an IV:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "target.simpleserial_write(0x00, bytearray(range(16)))\n",
    "target.simpleserial_wait_ack()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The bootloader protects against rewriting the IV, so if you rerun the block above the target won't update the IV and it'll return `0x12` as an error code.\n",
    "\n",
    "Let's start by seeing what the power traces for a correctly authenticated message:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#Do glitch loop\n",
    "key, text = ktp.next()\n",
    "text.extend([bootloader_calc_crc(text)])\n",
    "scope.arm()\n",
    "target.simpleserial_write(\"p\", text)\n",
    "scope.capture()\n",
    "val = target.simpleserial_wait_ack() # For loop check\n",
    "\n",
    "#print(bytearray(val['full_response'].encode('latin-1')))\n",
    "print(val)\n",
    "cw.plot(scope.get_last_trace())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "and an incorrectly authenticated one:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#Do glitch loop\n",
    "key, text = ktp.next()\n",
    "text.extend(???)\n",
    "scope.arm()\n",
    "target.simpleserial_write(\"p\", text)\n",
    "scope.capture()\n",
    "val = target.simpleserial_wait_ack()#For loop check\n",
    "\n",
    "#print(bytearray(val['full_response'].encode('latin-1')))\n",
    "print(val)\n",
    "cw.plot(scope.get_last_trace())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see, the target only decrypts our message if it's got the right MAC. If the target didn't tell us that our MAC was wrong, we could use to help this determine if we got our glitch or not.\n",
    "\n",
    "Next, we need to setup our glitch. By this point, you should have some fairly reliable settings you can use:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "scope.cglitch_setup(default_setup=False) # make sure default_setup is off so we don't overwrite our adc settings\n",
    "\n",
    "gc = cw.GlitchController(groups=[\"success\", \"reset\", \"normal\"], parameters=[\"width\", \"offset\", \"ext_offset\"])\n",
    "gc.display_stats()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "if scope._is_husky:\n",
    "    gc.set_range(\"width\", 3500, 4500)\n",
    "    gc.set_range(\"offset\", 1300, 3200)\n",
    "    gc.set_global_step([400, 200, 100])\n",
    "    gc.set_range(\"ext_offset\", 60, 100)\n",
    "    gc.set_step(\"ext_offset\", 1)\n",
    "    gc.set_range(\"ext_offset\", 6900//4, 7300//4)\n",
    "else:\n",
    "    if PLATFORM == \"CWLITEXMEGA\":\n",
    "        gc.set_range(\"width\", 0.4, 5)\n",
    "        gc.set_range(\"offset\", -0.4, -5.2)\n",
    "        gc.set_range(\"ext_offset\", 4, 70)\n",
    "        gc.set_range(\"ext_offset\", (7500+2000)//4, (7500+3500)//4)\n",
    "        sample_size = 50\n",
    "    elif PLATFORM == \"CW308_STM32F4\":\n",
    "        gc.set_range(\"width\", 0.4, 10)\n",
    "        gc.set_range(\"offset\", 40, 49.8)\n",
    "    elif PLATFORM == \"CWLITEARM\":\n",
    "        gc.set_range(\"width\", 0.8, 3.6)\n",
    "        gc.set_range(\"offset\", -4, -2)\n",
    "        gc.set_global_step(0.4)\n",
    "        \n",
    "gc.set_range(\"ext_offset\", 5000//4, 8000//4)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "gc.glitch_plot(plotdots={\"success\":\"+g\", \"reset\":\"xr\", \"normal\":None}, bufferlen=int(100E6))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We might get some \"successful\" glitches here that don't actually bypass the authentication. Therefore, let's get say 10 or so potentially successful glitches then check them afterwards for a good offset."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import struct\n",
    "\n",
    "scope.glitch.repeat = 1\n",
    "scope.adc.timeout = 0.1\n",
    "\n",
    "reboot_flush()\n",
    "target.reset_comms()\n",
    "x = []\n",
    "req_encs = 10\n",
    "encs = 0\n",
    "\n",
    "project = cw.create_project(\"projects/test_bootloader\", overwrite=True)\n",
    "\n",
    "for glitch_setting in gc.glitch_values():\n",
    "\n",
    "    # optional: you can speed up the loop by checking if the trigger never went low\n",
    "    #           (the target never called trigger_low();) via scope.adc.state\n",
    "    if encs > req_encs:\n",
    "        break\n",
    "    scope.glitch.width = glitch_setting[0]\n",
    "    scope.glitch.offset = glitch_setting[1]\n",
    "    scope.glitch.ext_offset = glitch_setting[2]\n",
    "    if scope.adc.state:\n",
    "        # can detect crash here (fast) before timing out (slow)\n",
    "        print(\"Trigger still high!\")\n",
    "        gc.add(\"reset\")\n",
    "        reboot_flush()\n",
    "        #target.reset_comms()\n",
    "\n",
    "    #Do glitch loop\n",
    "    key, text = ktp.next()\n",
    "    cpy_text = bytearray(text)\n",
    "    text.extend([0x00])\n",
    "    scope.arm()\n",
    "    target.simpleserial_write(\"p\", text)\n",
    "    ret = scope.capture()\n",
    "    val = target.simpleserial_read('e', 1, ack=False)\n",
    "    #print(val)\n",
    "\n",
    "    if ret: #here the trigger never went high - sometimes the target is stil crashed from a previous glitch\n",
    "        print('Timeout - no trigger')\n",
    "        gc.add(\"reset\")\n",
    "\n",
    "        #Device is slow to boot?\n",
    "        reboot_flush()\n",
    "        target.reset_comms()\n",
    "    else:\n",
    "        if val is None: # change this to detect an invalid response\n",
    "            gc.add(\"reset\")\n",
    "        else:\n",
    "            # gcnt is the loop counter\n",
    "            gcnt = val[0]\n",
    "\n",
    "            if gcnt == 0x11:#normal response\n",
    "                gc.add(\"normal\")\n",
    "            elif gcnt == 0x00: #glitch!!!\n",
    "                gc.add(\"success\")\n",
    "                print(f\"Loc: {scope.glitch.ext_offset}, Glitch number: {encs}\")\n",
    "                x.append(scope.get_last_trace())\n",
    "                trace = cw.Trace(scope.get_last_trace(), cpy_text, key, key)\n",
    "                project.traces.append(trace)\n",
    "                encs += 1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's plot these glitches:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot = cw.plot(x[0])\n",
    "for y in x[1:]:\n",
    "    plot += cw.plot(y)\n",
    "plot"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Hopefully you got at least one glitch that looks like the authenticated version of the encryption. We were able to use the return message to narrow down our glitches, but these other glitches shows that power traces are still very useful here.\n",
    "\n",
    "Now that we know where to insert a glitch to bypass the authentication, we could use one of those attacks on unauthenticated encryption. Instead, let's do a CPA attack. That way, we'll be able to insert as much code without the limitations of those other attacks. Start by finding an offset that bypasses the authentication:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cw.plot(x[2])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "gc.display_stats()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next, let's repeat the attack, with the objective this time being to gather enough encryptions to recover the key:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "if scope._is_husky:\n",
    "    scope.glitch.ext_offset = 1721\n",
    "    scope.adc.offset = 16000\n",
    "    scope.adc.samples = 5000\n",
    "    req_encs = 300\n",
    "    #scope.adc.offset = 7500\n",
    "elif PLATFORM==\"CWLITEARM\":\n",
    "    scope.glitch.ext_offset = 1767\n",
    "    scope.adc.offset = 5500\n",
    "    scope.adc.samples = 10000\n",
    "    req_encs = 300\n",
    "elif PLATFORM==\"CWLITEXMEGA\":\n",
    "    scope.glitch.ext_offset = 1865\n",
    "    scope.adc.offset = 12000\n",
    "    scope.adc.samples = 24400\n",
    "    req_encs = 300\n",
    "    \n",
    "if scope._is_husky:\n",
    "    gc.set_range(\"width\", 3500, 4500)\n",
    "    gc.set_range(\"offset\", 1300, 3200)\n",
    "    gc.set_global_step([400, 200, 100])\n",
    "    gc.set_range(\"ext_offset\", 60, 100)\n",
    "    gc.set_step(\"ext_offset\", 1)\n",
    "else:\n",
    "    if PLATFORM == \"CWLITEXMEGA\":\n",
    "        gc.set_range(\"width\", 0.4, 5)\n",
    "        gc.set_range(\"offset\", -0.4, -5.2)\n",
    "        gc.set_range(\"ext_offset\", 1, 100000000)\n",
    "        sample_size = 10\n",
    "    elif PLATFORM == \"CW308_STM32F4\":\n",
    "        gc.set_range(\"width\", 0.4, 10)\n",
    "        gc.set_range(\"offset\", 40, 49.8)\n",
    "    elif PLATFORM == \"CWLITEARM\":\n",
    "        gc.set_range(\"width\", 0.8, 3.6)\n",
    "        gc.set_range(\"offset\", -4, -2)\n",
    "        gc.set_global_step(0.4)\n",
    "        gc.set_range(\"ext_offset\", 0, 1000000)\n",
    "        gc.set_step(\"ext_offset\", 1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "\n",
    "scope.glitch.ext_offset = 2\n",
    "\n",
    "gc.set_range(\"ext_offset\", 0, 10)\n",
    "#gc.set_global_step([1])\n",
    "scope.glitch.repeat = 1\n",
    "\n",
    "scope.adc.timeout = 0.1\n",
    "\n",
    "reboot_flush()\n",
    "target.reset_comms()\n",
    "x = []\n",
    "req_encs = 100\n",
    "scope.adc.offset = 10000\n",
    "encs = 0\n",
    "\n",
    "\n",
    "\n",
    "project = cw.create_project(\"projects/test_bootloader\", overwrite=True)\n",
    "\n",
    "for glitch_setting in gc.glitch_values():\n",
    "\n",
    "    # optional: you can speed up the loop by checking if the trigger never went low\n",
    "    #           (the target never called trigger_low();) via scope.adc.state\n",
    "    if encs > req_encs:\n",
    "        break\n",
    "\n",
    "    # optional: you can speed up the loop by checking if the trigger never went low\n",
    "    #           (the target never called trigger_low();) via scope.adc.state\n",
    "    scope.glitch.width = glitch_setting[0]\n",
    "    scope.glitch.offset = glitch_setting[1]\n",
    "    if scope.adc.state:\n",
    "        # can detect crash here (fast) before timing out (slow)\n",
    "        print(\"Trigger still high!\")\n",
    "        gc.add(\"reset\")\n",
    "        reboot_flush()\n",
    "        target.reset_comms()\n",
    "\n",
    "    #Do glitch loop\n",
    "    key, text = ktp.next()\n",
    "    cpy_text = bytearray(text)\n",
    "    text.extend([0x00])\n",
    "    scope.arm()\n",
    "    target.simpleserial_write(\"p\", text)\n",
    "    ret = scope.capture()\n",
    "    val = target.simpleserial_read('e', 1, ack=False, timeout=50)\n",
    "    #print(val)\n",
    "\n",
    "    if ret: #here the trigger never went high - sometimes the target is stil crashed from a previous glitch\n",
    "        print('Timeout - no trigger')\n",
    "        gc.add(\"reset\")\n",
    "\n",
    "        #Device is slow to boot?\n",
    "        reboot_flush()\n",
    "        target.reset_comms()\n",
    "    else:\n",
    "        if val is None: # change this to detect an invalid response\n",
    "            gc.add(\"reset\")\n",
    "        else:\n",
    "            # gcnt is the loop counter\n",
    "            gcnt = val[0]\n",
    "\n",
    "            if gcnt == 0x11:#normal response\n",
    "                gc.add(\"normal\")\n",
    "            else: #glitch!!!\n",
    "                gc.add(\"success\")\n",
    "                print(scope.glitch.ext_offset)\n",
    "                print(encs)\n",
    "                x.append(scope.get_last_trace())\n",
    "                trace = cw.Trace(scope.get_last_trace(), cpy_text, key, key)\n",
    "                print(scope.glitch.width, scope.glitch.offset)\n",
    "                project.traces.append(trace)\n",
    "                encs += 1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's do a sanity check to make sure these traces all have encryptions (this will take a while to plot so you might only want to plot a subset of the traces)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "project.save()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot = cw.plot([])\n",
    "for y in range(10):\n",
    "    plot *= cw.plot(x[y])\n",
    "plot"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "AES decryption and encryption are actually very similar, except with all the forward function replaced with inverse functions. For example, instead of the SBox, we have the inverse SBox, instead of MixColumns we have inverse MixColumns, etc. As such our attack will use the inverse SBox instead of the regular SBox (though we'll use the alternate model so the correct key gets highlighted in the table):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import chipwhisperer as cw\n",
    "project = cw.open_project(\"projects/test_bootloader\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot = cw.plot([])\n",
    "for y in range(10):\n",
    "    plot *= cw.plot(project.waves[y])\n",
    "plot"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Depending on your target, you may need to resync your traces:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import chipwhisperer.analyzer as cwa\n",
    "resync_traces = cwa.preprocessing.ResyncSAD(project)\n",
    "resync_traces.ref_trace = 0\n",
    "resync_traces.target_window = (14000, 15000)\n",
    "resync_traces.max_shift = 200\n",
    "resync_analyzer = resync_traces.preprocess()\n",
    "plot = cw.plot([])\n",
    "for y in range(10):\n",
    "    plot *= cw.plot(resync_analyzer.waves[y])\n",
    "plot"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import chipwhisperer.analyzer as cwa\n",
    "leak_model = cwa.leakage_models.inverse_sbox_output_alt\n",
    "#attack = cwa.cpa(project, leak_model)\n",
    "attack = cwa.cpa(resync_analyzer, leak_model)\n",
    "results = attack.run(cwa.get_jupyter_callback(attack))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This won't actually recover our usual encryption key. Instead we've got the last round key. We can use ChipWhisperer to convert it back to the original key:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cw.bytearray(cwa.aes_funcs.key_schedule_rounds(results.key_guess(), 10, 0))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And we can see it's our usual 0x2b7e... key."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Injecting Malicious Code (Advanced)\n",
    "\n",
    "Now that we can both bypass the MAC, as well as encrypt messages, try creating a piece of malicious code to dump firmware. Following commands will be useful:\n",
    "\n",
    "1. `set_addr()` sets the address to write to\n",
    "1. `go()` will run code pointed to by `set_addr()`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "scope.dis()\n",
    "target.dis()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
